Basic Pattern: Blocking Events
Learn about the basic pattern of handling blocking events.
We'll cover the following
Blocking nature of read/write operations#
The most used source of events is I/O readiness. Most read and write operations
are, by default, blocking in nature which slows down the program execution speed. If the program has to wait several seconds for a read to be completed, it cannot do anything else during that time. read is a synchronous call and when performed on a file, socket, etc., that has no data ready to be read, it blocks the program.
Solution: Don’t just wait!#
The solution to that problem is to expect an event when the socket, for example, is ready to be read. While this is not the case, the program can deal with any other event that might happen.
The following example shows a simple program that sends an HTTP request to http://httpbin.org/delay/5. The URL returns a JSON content after five seconds of delay.
As expected, when this program runs, it takes at least five seconds to complete:
the socket.recv call hangs until the remote Web server sends the reply.
This is the kind of situation that should be avoided: waiting for an input or output to complete before going on, as the program could be doing something else rather than waiting.
The solution here is to put the socket in asynchronous mode. This can be done using the setblocking method.
The following application is not going to output the expected result on purpose.
Running the above example will fail with an interesting error: BlockingIOError: [Errno 35] Resource temporarily unavailable.
As the socket does not have any data to be read, rather than blocking (until it has the data), Python raises a BlockingIOError, asking the caller to retry at a later time.
At this point, you can see where this is going. If the program can get a message as soon as the socket is ready to be handled, the code can do something else rather than actively waiting.
The select module#
The simplest mechanism to make the code do something else rather than actively waiting is to use the select module in Python. This module provides select.select(rlist, wlist, xlist) function that takes any number of sockets (or file descriptors) as input and returns the ones that are ready to read,
write or have errors.
In the above example, the socket is passed as an argument in the list of descriptors we want to watch for read-readiness. As soon as the socket has data available to read, the program can read them without blocking.
If you combine multiple sources of events in a select call, it is easy to see how your program can become event-driven. The select loop becomes the main control flow of the program, and everything revolves around it. As soon as some file descriptor or socket is available for reading or writing, it is possible to continue operating on it.
This kind of mechanism is at the heart of any program that wants to handle, for example, thousands of connections at once. It is the base technology leveraged by tools such as really fast HTTP servers like NGINX or Node.js.
select is an old but generic system call, and it is not the highest performing out there. Different operating systems implement various alternatives and optimizations, such as epoll in Linux or kqueue in FreeBSD. As Python is a high-level language, it implements and provides an abstraction layer known as asyncio.
Introduction to Event Loops
Using Asyncio